001 /* 002 * Copyright 2003-2005 The Apache Software Foundation 003 * Copyright 2005 Stephen McConnell 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package net.dpml.cli.option; 018 019 import java.util.Collections; 020 import java.util.Comparator; 021 import java.util.List; 022 import java.util.ListIterator; 023 import java.util.Set; 024 import java.util.StringTokenizer; 025 026 import net.dpml.cli.Argument; 027 import net.dpml.cli.DisplaySetting; 028 import net.dpml.cli.HelpLine; 029 import net.dpml.cli.Option; 030 import net.dpml.cli.OptionException; 031 import net.dpml.cli.WriteableCommandLine; 032 import net.dpml.cli.resource.ResourceConstants; 033 import net.dpml.cli.resource.ResourceHelper; 034 import net.dpml.cli.validation.InvalidArgumentException; 035 import net.dpml.cli.validation.Validator; 036 037 /** 038 * An implementation of an Argument. 039 * @author <a href="http://www.dpml.net">Digital Product Meta Library</a> 040 * @version 1.0.0 041 */ 042 public class ArgumentImpl extends OptionImpl implements Argument 043 { 044 private static final char NUL = '\0'; 045 046 /** 047 * The default value for the initial separator char. 048 */ 049 public static final char DEFAULT_INITIAL_SEPARATOR = NUL; 050 051 /** 052 * The default value for the subsequent separator char. 053 */ 054 public static final char DEFAULT_SUBSEQUENT_SEPARATOR = NUL; 055 056 /** 057 * The default token to indicate that remaining arguments should be consumed 058 * as values. 059 */ 060 public static final String DEFAULT_CONSUME_REMAINING = "--"; 061 062 private final String m_name; 063 private final String m_description; 064 private final int m_minimum; 065 private final int m_maximum; 066 private final char m_initialSeparator; 067 private final char m_subsequentSeparator; 068 private final boolean m_subsequentSplit; 069 private final Validator m_validator; 070 private final String m_consumeRemaining; 071 private final List m_defaultValues; 072 private final ResourceHelper m_resources = ResourceHelper.getResourceHelper(); 073 074 /** 075 * Creates a new Argument instance. 076 * 077 * @param name the name of the argument 078 * @param description a description of the argument 079 * @param minimum the minimum number of values needed to be valid 080 * @param maximum the maximum number of values allowed to be valid 081 * @param initialSeparator the char separating option from value 082 * @param subsequentSeparator the char separating values from each other 083 * @param validator object responsible for validating the values 084 * @param consumeRemaining String used for the "consuming option" group 085 * @param valueDefaults values to be used if none are specified. 086 * @param id the id of the option, 0 implies automatic assignment. 087 * 088 * @see OptionImpl#OptionImpl(int,boolean) 089 */ 090 public ArgumentImpl( 091 final String name, final String description, final int minimum, final int maximum, 092 final char initialSeparator, final char subsequentSeparator, final Validator validator, 093 final String consumeRemaining, final List valueDefaults, final int id ) 094 { 095 super( id, false ); 096 097 m_description = description; 098 m_minimum = minimum; 099 m_maximum = maximum; 100 m_initialSeparator = initialSeparator; 101 m_subsequentSeparator = subsequentSeparator; 102 m_subsequentSplit = subsequentSeparator != NUL; 103 m_validator = validator; 104 m_consumeRemaining = consumeRemaining; 105 m_defaultValues = valueDefaults; 106 107 if( null == name ) 108 { 109 m_name = "arg"; 110 } 111 else 112 { 113 m_name = name; 114 } 115 116 if( m_minimum > m_maximum ) 117 { 118 throw new IllegalArgumentException( 119 m_resources.getMessage( 120 ResourceConstants.ARGUMENT_MIN_EXCEEDS_MAX ) ); 121 } 122 123 if( ( m_defaultValues != null ) && ( m_defaultValues.size() > 0 ) ) 124 { 125 if( valueDefaults.size() < minimum ) 126 { 127 throw new IllegalArgumentException( 128 m_resources.getMessage( 129 ResourceConstants.ARGUMENT_TOO_FEW_DEFAULTS ) ); 130 } 131 if( m_defaultValues.size() > maximum ) 132 { 133 throw new IllegalArgumentException( 134 m_resources.getMessage( 135 ResourceConstants.ARGUMENT_TOO_MANY_DEFAULTS ) ); 136 } 137 } 138 } 139 140 /** 141 * The preferred name of an option is used for generating help and usage 142 * information. 143 * 144 * @return The preferred name of the option 145 */ 146 public String getPreferredName() 147 { 148 return m_name; 149 } 150 151 /** 152 * Processes the "README" style element of the argument. 153 * 154 * Values identified should be added to the CommandLine object in 155 * association with this Argument. 156 * 157 * @see WriteableCommandLine#addValue(Option,Object) 158 * 159 * @param commandLine The CommandLine object to store results in. 160 * @param arguments The arguments to process. 161 * @param option The option to register value against. 162 * @throws OptionException if any problems occur. 163 */ 164 public void processValues( 165 final WriteableCommandLine commandLine, final ListIterator arguments, final Option option ) 166 throws OptionException 167 { 168 int argumentCount = commandLine.getValues( option, Collections.EMPTY_LIST ).size(); 169 170 while( arguments.hasNext() && ( argumentCount < m_maximum ) ) 171 { 172 final String allValues = stripBoundaryQuotes( (String) arguments.next() ); 173 174 // should we ignore things that look like options? 175 if( allValues.equals( m_consumeRemaining ) ) 176 { 177 while( arguments.hasNext() && ( argumentCount < m_maximum ) ) 178 { 179 ++argumentCount; 180 commandLine.addValue( option, arguments.next() ); 181 } 182 } 183 // does it look like an option? 184 else if( commandLine.looksLikeOption( allValues ) ) 185 { 186 arguments.previous(); 187 break; 188 } 189 // should we split the string up? 190 else if( m_subsequentSplit ) 191 { 192 final StringTokenizer values = 193 new StringTokenizer( allValues, String.valueOf( m_subsequentSeparator ) ); 194 arguments.remove(); 195 196 while( values.hasMoreTokens() && ( argumentCount < m_maximum ) ) 197 { 198 ++argumentCount; 199 final String token = values.nextToken(); 200 commandLine.addValue( option, token ); 201 arguments.add( token ); 202 } 203 204 if( values.hasMoreTokens() ) 205 { 206 throw new OptionException( 207 option, 208 ResourceConstants.ARGUMENT_UNEXPECTED_VALUE, 209 values.nextToken() ); 210 } 211 } 212 else 213 { 214 // it must be a value as it is 215 ++argumentCount; 216 commandLine.addValue( option, allValues ); 217 } 218 } 219 } 220 221 /** 222 * Indicates whether this Option will be able to process the particular 223 * argument. 224 * 225 * @param commandLine the CommandLine object to store defaults in 226 * @param argument the argument to be tested 227 * @return true if the argument can be processed by this Option 228 */ 229 public boolean canProcess( final WriteableCommandLine commandLine, final String argument ) 230 { 231 return true; 232 } 233 234 /** 235 * Identifies the argument prefixes that should be considered options. This 236 * is used to identify whether a given string looks like an option or an 237 * argument value. Typically an option would return the set [--,-] while 238 * switches might offer [-,+]. 239 * 240 * The returned Set must not be null. 241 * 242 * @return The set of prefixes for this Option 243 */ 244 public Set getPrefixes() 245 { 246 return Collections.EMPTY_SET; 247 } 248 249 /** 250 * Processes String arguments into a CommandLine. 251 * 252 * The iterator will initially point at the first argument to be processed 253 * and at the end of the method should point to the first argument not 254 * processed. This method MUST process at least one argument from the 255 * ListIterator. 256 * 257 * @param commandLine the CommandLine object to store results in 258 * @param args the arguments to process 259 * @throws OptionException if any problems occur 260 */ 261 public void process( WriteableCommandLine commandLine, ListIterator args ) 262 throws OptionException 263 { 264 processValues( commandLine, args, this ); 265 } 266 267 /** 268 * Returns the initial separator character or 269 * '\0' if no character has been set. 270 * 271 * @return char the initial separator character 272 */ 273 public char getInitialSeparator() 274 { 275 return m_initialSeparator; 276 } 277 278 /** 279 * Returns the subsequent separator character. 280 * 281 * @return the subsequent separator character 282 */ 283 public char getSubsequentSeparator() 284 { 285 return m_subsequentSeparator; 286 } 287 288 /** 289 * Identifies the argument prefixes that should trigger this option. This 290 * is used to decide which of many Options should be tried when processing 291 * a given argument string. 292 * 293 * The returned Set must not be null. 294 * 295 * @return The set of triggers for this Option 296 */ 297 public Set getTriggers() 298 { 299 return Collections.EMPTY_SET; 300 } 301 302 /** 303 * Return the consume remaining flag. 304 * @return the consume remaining flag 305 */ 306 public String getConsumeRemaining() 307 { 308 return m_consumeRemaining; 309 } 310 311 /** 312 * Return the list of default values. 313 * @return the default values 314 */ 315 public List getDefaultValues() 316 { 317 return m_defaultValues; 318 } 319 320 /** 321 * Return the argument validator. 322 * @return the validator 323 */ 324 public Validator getValidator() 325 { 326 return m_validator; 327 } 328 329 /** 330 * Performs any necessary validation on the values added to the 331 * CommandLine. 332 * 333 * Validation will typically involve using the 334 * CommandLine.getValues(option) method to retrieve the values 335 * and then either checking each value. Optionally the String 336 * value can be replaced by another Object such as a Number 337 * instance or a File instance. 338 * 339 * @see net.dpml.cli.CommandLine#getValues(Option) 340 * 341 * @param commandLine The CommandLine object to query. 342 * @throws OptionException if any problems occur. 343 */ 344 public void validate( final WriteableCommandLine commandLine ) throws OptionException 345 { 346 validate( commandLine, this ); 347 } 348 349 /** 350 * Performs any necessary validation on the values added to the 351 * CommandLine. 352 * 353 * Validation will typically involve using the 354 * CommandLine.getValues(option) method to retrieve the values 355 * and then either checking each value. Optionally the String 356 * value can be replaced by another Object such as a Number 357 * instance or a File instance. 358 * 359 * @see net.dpml.cli.CommandLine#getValues(Option) 360 * 361 * @param commandLine The CommandLine object to query. 362 * @param option The option to lookup values with. 363 * @throws OptionException if any problems occur. 364 */ 365 public void validate( 366 final WriteableCommandLine commandLine, final Option option ) 367 throws OptionException 368 { 369 final List values = commandLine.getValues( option ); 370 if( values.size() < m_minimum ) 371 { 372 throw new OptionException( 373 option, 374 ResourceConstants.ARGUMENT_MISSING_VALUES ); 375 } 376 377 if( values.size() > m_maximum ) 378 { 379 throw new OptionException( 380 option, 381 ResourceConstants.ARGUMENT_UNEXPECTED_VALUE, 382 (String) values.get( m_maximum ) ); 383 } 384 385 if( m_validator != null ) 386 { 387 try 388 { 389 m_validator.validate( values ); 390 } 391 catch( InvalidArgumentException ive ) 392 { 393 throw new OptionException( 394 option, 395 ResourceConstants.ARGUMENT_UNEXPECTED_VALUE, 396 ive.getMessage() ); 397 } 398 } 399 } 400 401 /** 402 * Appends usage information to the specified StringBuffer 403 * 404 * @param buffer the buffer to append to 405 * @param helpSettings a set of display settings @see DisplaySetting 406 * @param comp a comparator used to sort the Options 407 */ 408 public void appendUsage( 409 final StringBuffer buffer, final Set helpSettings, final Comparator comp ) 410 { 411 // do we display the outer optionality 412 final boolean optional = helpSettings.contains( DisplaySetting.DISPLAY_OPTIONAL ); 413 414 // allow numbering if multiple args 415 final boolean numbered = 416 ( m_maximum > 1 ) 417 && helpSettings.contains( DisplaySetting.DISPLAY_ARGUMENT_NUMBERED ); 418 419 final boolean bracketed = helpSettings.contains( DisplaySetting.DISPLAY_ARGUMENT_BRACKETED ); 420 421 // if infinite args are allowed then crop the list 422 final int max = getMaxValue(); 423 424 int i = 0; 425 426 // for each argument 427 while( i < max ) 428 { 429 // if we're past the first add a space 430 if( i > 0 ) 431 { 432 buffer.append( ' ' ); 433 } 434 435 // if the next arg is optional 436 if( ( i >= m_minimum ) && ( optional || ( i > 0 ) ) ) 437 { 438 buffer.append( '[' ); 439 } 440 441 if( bracketed ) 442 { 443 buffer.append( '<' ); 444 } 445 446 // add name 447 buffer.append( m_name ); 448 ++i; 449 450 // if numbering 451 if( numbered ) 452 { 453 buffer.append( i ); 454 } 455 456 if( bracketed ) 457 { 458 buffer.append( '>' ); 459 } 460 } 461 462 // if infinite args are allowed 463 if( m_maximum == Integer.MAX_VALUE ) 464 { 465 // append elipsis 466 buffer.append( " ..." ); 467 } 468 469 // for each argument 470 while( i > 0 ) 471 { 472 --i; 473 // if the next arg is optional 474 if( ( i >= m_minimum ) && ( optional || ( i > 0 ) ) ) 475 { 476 buffer.append( ']' ); 477 } 478 } 479 } 480 481 /** 482 * Returns a description of the option. This string is used to build help 483 * messages as in the HelpFormatter. 484 * 485 * @see net.dpml.cli.util.HelpFormatter 486 * @return a description of the option. 487 */ 488 public String getDescription() 489 { 490 return m_description; 491 } 492 493 /** 494 * Builds up a list of HelpLineImpl instances to be presented by HelpFormatter. 495 * 496 * @see HelpLine 497 * @see net.dpml.cli.util.HelpFormatter 498 * @param depth the initial indent depth 499 * @param helpSettings the HelpSettings that should be applied 500 * @param comp a comparator used to sort options when applicable. 501 * @return a List of HelpLineImpl objects 502 */ 503 public List helpLines( final int depth, final Set helpSettings, final Comparator comp ) 504 { 505 final HelpLine helpLine = new HelpLineImpl( this, depth ); 506 return Collections.singletonList( helpLine ); 507 } 508 509 /** 510 * Retrieves the maximum number of values acceptable for a valid Argument 511 * 512 * @return the maximum number of values 513 */ 514 public int getMaximum() 515 { 516 return m_maximum; 517 } 518 519 /** 520 * Retrieves the minimum number of values required for a valid Argument 521 * 522 * @return the minimum number of values 523 */ 524 public int getMinimum() 525 { 526 return m_minimum; 527 } 528 529 /** 530 * If there are any leading or trailing quotes remove them from the 531 * specified token. 532 * 533 * @param token the token to strip leading and trailing quotes 534 * @return String the possibly modified token 535 */ 536 public String stripBoundaryQuotes( String token ) 537 { 538 if( !token.startsWith( "\"" ) || !token.endsWith( "\"" ) ) 539 { 540 return token; 541 } 542 token = token.substring( 1, token.length() - 1 ); 543 return token; 544 } 545 546 /** 547 * Indicates whether argument values must be present for the CommandLine to 548 * be valid. 549 * 550 * @see #getMinimum() 551 * @see #getMaximum() 552 * @return true iff the CommandLine will be invalid without at least one 553 * value 554 */ 555 public boolean isRequired() 556 { 557 return getMinimum() > 0; 558 } 559 560 /** 561 * Adds defaults to a CommandLine. 562 * 563 * @param commandLine the CommandLine object to store defaults in. 564 */ 565 public void defaults( final WriteableCommandLine commandLine ) 566 { 567 super.defaults( commandLine ); 568 defaultValues( commandLine, this ); 569 } 570 571 /** 572 * Adds defaults to a CommandLine. 573 * 574 * @param commandLine the CommandLine object to store defaults in. 575 * @param option the Option to store the defaults against. 576 */ 577 public void defaultValues( final WriteableCommandLine commandLine, final Option option ) 578 { 579 commandLine.setDefaultValues( option, m_defaultValues ); 580 } 581 582 private int getMaxValue() 583 { 584 if( m_maximum == Integer.MAX_VALUE ) 585 { 586 return 2; 587 } 588 else 589 { 590 return m_maximum; 591 } 592 } 593 594 }